package client_core

import (
	"context"
	"crypto/sha1"
	"errors"
	"fmt"
	"github.com/chicken-team-outside/chicken_transmission/api_messages"
	"github.com/chicken-team-outside/chicken_transmission/model"
	"github.com/chicken-team-outside/chicken_transmission/sfs"
	"github.com/chicken-team-outside/chicken_transmission/utils"
	"github.com/golang/protobuf/proto"
	"go.uber.org/zap"
	"io"
	"os"
	"sync"
)

const MaxTaskStatusSize = 1024 * 1024 * 10

func GetTmpFilePath(filePath *string) string {
	return *filePath + ".kc_tmp"
}

func GetStateFilePath(filePath *string) string {
	return *filePath + ".kc_st"
}

func DeleteStatusFileAndTmpFile(filePath *string) error {
	tmpFile := GetTmpFilePath(filePath)
	err := os.Remove(tmpFile)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("error deleting status file: %w", err)
	}
	statFile := GetStateFilePath(filePath)
	err = os.Remove(statFile)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("error deleting status file: %w", err)
	}
	return nil
}

type DownloadConnection struct {
	blockSize int32
	task      *model.Task
	ctx       context.Context
	block     *api_messages.TaskBlockInfo
	response  io.ReadCloser
	err       error
	lock      sync.Mutex
	start     int64
	end       int64
	retries   int32
}

func (c *DownloadConnection) connect() {
	client := GetSfsClient()
	if client == nil {
		c.err = ErrSFSClientNotAvailable
		return
	}
	defer c.lock.Unlock()
	var readHandler *sfs.ReaderHandler
	size := c.end - c.start
	readHandler, c.err = client.ReadFile(c.ctx, c.task.DeviceId, c.task.DevicePath, c.start, size, c.block.Sha1)
	if c.err != nil {
		c.err = fmt.Errorf("failed to read file block: %w", c.err)
		return
	}
	c.response, c.err = readHandler.OpenReader(c.ctx, c.block.Offset-c.start, c.end-c.start)
	if c.err != nil {
		c.err = fmt.Errorf("failed open block reader: %w", c.err)
	}
}

func (c *DownloadConnection) close() error {
	if c.response == nil {
		return nil
	}
	err := c.response.Close()
	return err
}

type Downloader struct {
	ctx        context.Context
	cancel     context.CancelCauseFunc
	task       *TaskWarp
	status     *api_messages.DownloadTaskStatus
	blockSize  int32
	tmpFd      *os.File
	connPool   []*DownloadConnection
	taskList   []*api_messages.TaskBlockInfo
	buf        []byte
	maxWorkers int32
	retries    int32
}

func (d *Downloader) GetTmpFilePath() string {
	return GetTmpFilePath(&d.task.info.LocalPath)
}

func (d *Downloader) GetStateFilePath() string {
	return GetStateFilePath(&d.task.info.LocalPath)
}

func (d *Downloader) saveStat() error {
	data, err := proto.Marshal(d.status)
	if err != nil {
		return err
	}
	file, err := os.OpenFile(d.GetStateFilePath(), os.O_WRONLY|os.O_CREATE, os.ModePerm)
	if err != nil {
		return err
	}
	defer file.Close()
	err = file.Truncate(int64(len(data)))
	if err != nil {
		return err
	}
	_, err = file.Write(data)
	if err != nil {
		return err
	}
	return file.Sync()
}

func (d *Downloader) createStat() error {
	err := d.task.SetMsg("calculate file sha1")
	if err != nil {
		return fmt.Errorf("failed to update stat msg: %w", err)
	}
	client := GetSfsClient()
	if client == nil {
		return ErrSFSClientNotAvailable
	}
	sha1sInfo, err := client.CalculateFileBlocksSha1(d.ctx, d.task.info.DeviceId, d.task.info.DevicePath, d.blockSize)
	if err != nil {
		return fmt.Errorf("failed to calculate file blocks: %w", err)
	}
	d.task.SetCompleted(0)
	d.task.SetTotal(sha1sInfo.Size)
	d.task.UpdateProgress()
	d.status = &api_messages.DownloadTaskStatus{
		Size:      sha1sInfo.Size,
		BlockSize: d.blockSize,
		Blocks:    make([]*api_messages.TaskBlockInfo, len(sha1sInfo.Sha1List)/sha1.Size),
	}
	for i := 0; i < len(d.status.Blocks); i++ {
		d.status.Blocks[i] = &api_messages.TaskBlockInfo{
			Id:     int64(i),
			Offset: int64(i) * int64(d.blockSize),
			Sha1:   sha1sInfo.Sha1List[i*sha1.Size : (i+1)*sha1.Size],
		}
	}
	err = d.saveStat()
	if err != nil {
		return fmt.Errorf("failed to save status: %w", err)
	}
	return nil
}

func (d *Downloader) getCompletedSize() (size int64) {
	for _, item := range d.status.Blocks {
		start := item.Id * int64(d.status.BlockSize)
		size += item.Offset - start
	}
	return
}

func (d *Downloader) loadStat() error {
	stPath := d.GetStateFilePath()
	if stat, err := os.Stat(stPath); err == nil {
		if stat.IsDir() {
			return fmt.Errorf("stat path \"%s\" is a dir: %w", stPath, os.ErrPermission)
		} else if stat.Size() < MaxTaskStatusSize {
			file, err := os.OpenFile(stPath, os.O_RDWR, os.ModePerm)
			if err != nil {
				return fmt.Errorf("error opening status file: %w", err)
			}
			defer file.Close()
			buf := make([]byte, stat.Size())
			_, err = io.ReadFull(file, buf)
			if err == nil {
				d.status = new(api_messages.DownloadTaskStatus)
				err = proto.Unmarshal(buf, d.status)
			}
			if err == nil {
				if d.task.GetTotal() != d.status.Size {
					err = fmt.Errorf("task file size %d not match stat file size %d", d.task.GetTotal(), d.status.Size)
				} else {
					d.task.SetCompleted(d.getCompletedSize())
					return nil
				}
			}
			zap.L().Warn("error load task status from file. It will be recreated.", zap.Error(err), zap.String("path", stPath))
		} else {
			zap.L().Warn("status file size exceed max size", zap.Int64("size", stat.Size()), zap.Int64("max size", MaxTaskStatusSize))
		}
	}
	return d.createStat()
}

func (d *Downloader) createNewConn() *DownloadConnection {
	for i := 0; i < len(d.taskList); i++ {
		info := d.taskList[i]
		start := info.Id * int64(d.blockSize)
		end := start + int64(d.blockSize)
		if end > d.task.info.Total {
			end = d.task.info.Total
		}
		if info.Offset < end {
			d.taskList = d.taskList[i+1:]
			return &DownloadConnection{
				ctx:       d.ctx,
				blockSize: d.blockSize,
				task:      d.task.info,
				block:     info,
				start:     start,
				end:       end,
			}
		}
	}
	d.taskList = nil
	return nil
}

func (d *Downloader) checkAndRetry(conn *DownloadConnection) bool {
	conn.close()
	if conn.ctx.Err() == nil && conn.retries < d.retries {
		conn.retries++
		conn.err = nil
		go conn.connect()
		return true
	} else {
		return false
	}
}

func (d *Downloader) saveDataFromConn(index int, conn *DownloadConnection) (err error, retry bool) {
	var readN int
	readN, err = conn.response.Read(d.buf)
	if readN > 0 {
		_, err01 := d.tmpFd.Seek(conn.block.Offset, io.SeekStart)
		if err01 == nil {
			_, err01 = d.tmpFd.Write(d.buf[:readN])
		}
		if err01 != nil {
			err = fmt.Errorf("error to write data to tmp file: %w", err01)
		} else {
			conn.block.Offset += int64(readN)
			d.task.AddCompleted(int64(readN))
		}
	}
	if conn.block.Offset >= conn.end {
		conn.close()
		d.connPool[index] = nil
		err = nil
	} else if err != nil {
		if d.checkAndRetry(conn) {
			retry = true
			err = nil
		}
	}
	return
}

func (d *Downloader) doReceiveData() (err error) {
	for i, conn := range d.connPool {
		if conn != nil && conn.lock.TryLock() {
			retry := false
			if conn.err != nil {
				if !d.checkAndRetry(conn) {
					err = conn.err
				} else {
					continue
				}
			} else {
				err, retry = d.saveDataFromConn(i, conn)
			}
			if !retry {
				conn.lock.Unlock()
			}
			if err != nil {
				conn.close()
				break
			}
		}
	}
	return
}

func (d *Downloader) checkConn() (count int) {
	for i := 0; i < len(d.connPool); i++ {
		if d.connPool[i] == nil {
			conn := d.createNewConn()
			if conn != nil {
				d.connPool[i] = conn
				conn.lock.Lock()
				go conn.connect()
			}
		}
		if d.connPool[i] != nil {
			count++
		}
	}
	return
}

func (d *Downloader) close() error {
	if d.tmpFd != nil {
		if err := d.tmpFd.Sync(); err != nil {
			if !errors.Is(err, os.ErrClosed) {
				zap.L().Warn("failed to sync temporary file", zap.Error(err), zap.String("path", d.GetTmpFilePath()))
			}
		}
		if err := d.tmpFd.Close(); err != nil {
			if !errors.Is(err, os.ErrClosed) {
				return err
			}
		}
		d.tmpFd = nil
	}
	return nil
}

func (d *Downloader) finish() error {
	err := d.close()
	if err != nil {
		return fmt.Errorf("error close tmp file: %w", err)
	}
	err = os.Rename(d.GetTmpFilePath(), d.task.info.LocalPath)
	if err != nil {
		return fmt.Errorf("error rename tmp file: %w", err)
	}
	err = os.Remove(d.GetStateFilePath())
	if err != nil {
		zap.L().Warn("error removing stat file", zap.Error(err), zap.String("path", d.GetStateFilePath()))
	}
	return nil
}

func (d *Downloader) Run() (err error) {
	defer d.close()
	d.connPool = make([]*DownloadConnection, d.maxWorkers)
	last := d.task.GetCompleted()
	for d.ctx.Err() == nil {
		if d.checkConn() <= 0 {
			break
		}
		err = d.doReceiveData()
		if err != nil {
			d.cancel(err)
			break
		}
		current := d.task.GetCompleted()
		if current-last > 300*1024 {
			zap.L().Debug("download ...", zap.Int64("complete", current/1024), zap.Int64("total", d.task.GetTotal()/1024), zap.String("rate", fmt.Sprintf("%.2f%%", float64(current)/float64(d.task.GetTotal())*100)))
			last = current
		}
	}
	for _, conn := range d.connPool {
		if conn != nil {
			conn.lock.Lock()
			if conn.response != nil {
				conn.response.Close()
			}
		}
	}
	err = context.Cause(d.ctx)
	if err == nil {
		return d.finish()
	}
	if err01 := d.saveStat(); err01 != nil {
		zap.L().Error("failed to save stat", zap.Error(err01), zap.String("path", d.GetStateFilePath()))
	}
	return err
}

func NewDownloader(task *TaskWarp, blockSize, maxWorkers, retries int32, buf []byte) (*Downloader, error) {
	dr := &Downloader{
		task:       task,
		blockSize:  blockSize,
		buf:        buf,
		maxWorkers: maxWorkers,
		retries:    retries,
	}
	dr.ctx, dr.cancel = context.WithCancelCause(task.ctx)
	var err error
	tmp := dr.GetTmpFilePath()
	var exist bool
	exist, err = utils.CheckPathAndCreateParent(tmp, true)
	if err == nil {
		if exist {
			dr.tmpFd, err = os.OpenFile(tmp, os.O_WRONLY, os.ModePerm)
		} else {
			dr.tmpFd, err = os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE, os.ModePerm)
		}
	}
	if err != nil {
		return nil, fmt.Errorf("error opening tmp file %s: %w", tmp, err)
	}
	err = dr.loadStat()
	if err != nil {
		dr.tmpFd.Close()
		return nil, err
	}
	err = dr.tmpFd.Truncate(dr.task.GetTotal())
	if err != nil {
		dr.tmpFd.Close()
		return nil, fmt.Errorf("error trunc tmp file %s: %w", tmp, err)
	}
	dr.taskList = dr.status.Blocks
	return dr, nil
}
